/*******************************************************************************
 * Copyright (c) 2005, 2023 IBM Corporation and others.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0.
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/
package org.eclipse.draw2d;

import java.util.List;

import org.eclipse.draw2d.geometry.Insets;
import org.eclipse.draw2d.geometry.PointList;
import org.eclipse.draw2d.geometry.Rectangle;

/**
 * Clipping strategy for connection layer, which takes into account nested view
 * ports and truncates those parts of connections which reach outside and are
 * thus not visible.
 *
 * @author Alexander Nyssen
 * @author Philip Ritzkopf
 *
 * @since 3.6
 */
public class ViewportAwareConnectionLayerClippingStrategy implements IClippingStrategy {

	private static final Insets PRIVATE_INSETS = new Insets(0, 0, 1, 1);

	private ConnectionLayer connectionLayer = null;

	public ViewportAwareConnectionLayerClippingStrategy(ConnectionLayer connectionLayer) {
		this.connectionLayer = connectionLayer;
	}

	/**
	 * @see org.eclipse.draw2d.IClippingStrategy#getClip(org.eclipse.draw2d.IFigure)
	 */
	@Override
	public Rectangle[] getClip(IFigure figure) {
		Rectangle[] clipRect = null;
		if (figure instanceof Connection conn) {
			clipRect = getEdgeClippingRectangle(conn);
		} else {
			clipRect = new Rectangle[] { getNodeClippingRectangle(figure) };
		}
		// translate clipping rectangles (which are in absolute coordinates)
		// to be relative to the parent figure's (i.e. the connection
		// layer's) client area
		for (Rectangle element : clipRect) {
			figure.translateToRelative(element);
		}
		return clipRect;
	}

	/**
	 * Computes clipping rectangle(s) for a given connection. Will consider all
	 * enclosing viewports, excluding the root viewport.
	 */
	protected Rectangle[] getEdgeClippingRectangle(Connection connection) {
		// start with clipping the connection at its original bounds
		Rectangle clipRect = getAbsoluteBoundsAsCopy(connection);

		// in case we cannot infer source and target of the connection (e.g.
		// if XYAnchors are used), returning the bounds is all we can do
		ConnectionAnchor sourceAnchor = connection.getSourceAnchor();
		ConnectionAnchor targetAnchor = connection.getTargetAnchor();
		if (sourceAnchor == null || sourceAnchor.getOwner() == null || targetAnchor == null
				|| targetAnchor.getOwner() == null) {
			return new Rectangle[] { clipRect };
		}

		// source and target figure are known, see if there is common
		// viewport
		// the connection has to be clipped at.
		IFigure sourceFigure = sourceAnchor.getOwner();
		IFigure targetFigure = targetAnchor.getOwner();
		Viewport nearestEnclosingCommonViewport = ViewportUtilities.getNearestCommonViewport(sourceFigure,
				targetFigure);
		if (nearestEnclosingCommonViewport == null) {
			return new Rectangle[] { clipRect };
		}

		// if the nearest common viewport is not the root viewport, we may
		// start with clipping the connection at this viewport.
		if (nearestEnclosingCommonViewport != getRootViewport()) {
			clipRect.intersect(getNodeClippingRectangle(nearestEnclosingCommonViewport));
		}

		// if the nearest common viewport of source and target is not
		// simultaneously
		// the nearest enclosing viewport of source and target respectively, the
		// connection has to be further clipped (the connection may even not be
		// visible at all)
		Viewport nearestEnclosingSourceViewport = ViewportUtilities.getNearestEnclosingViewport(sourceFigure);
		Viewport nearestEnclosingTargetViewport = ViewportUtilities.getNearestEnclosingViewport(targetFigure);
		if (nearestEnclosingSourceViewport == nearestEnclosingTargetViewport) {
			// source and target share the same enclosing viewport, so just
			// return what we have computed before (clipping at nearest
			// enclosing viewport)
			return new Rectangle[] { clipRect };
		}
		// compute if source and target anchor are visible
		// within the nearest common enclosing viewport (which may
		// itself be nested in other viewports).
		Rectangle sourceClipRect = clipRect.getCopy();
		if (nearestEnclosingSourceViewport != nearestEnclosingCommonViewport) {
			clipAtViewports(sourceClipRect, ViewportUtilities.getViewportsPath(nearestEnclosingSourceViewport,
					nearestEnclosingCommonViewport, false));
		}
		Rectangle targetClipRect = clipRect.getCopy();
		if (nearestEnclosingTargetViewport != nearestEnclosingCommonViewport) {
			clipAtViewports(targetClipRect, ViewportUtilities.getViewportsPath(nearestEnclosingTargetViewport,
					nearestEnclosingCommonViewport, false));
		}
		PointList absolutePointsAsCopy = getAbsolutePointsAsCopy(connection);
		boolean sourceAnchorVisible = sourceClipRect.getExpanded(PRIVATE_INSETS)
				.contains(absolutePointsAsCopy.getFirstPoint());
		boolean targetAnchorVisible = targetClipRect.getExpanded(PRIVATE_INSETS)
				.contains(absolutePointsAsCopy.getLastPoint());

		if (!sourceAnchorVisible || !targetAnchorVisible) {
			// one (or both) of source or target anchor is invisible
			// within the nearest common viewport, so up to now
			// we regard the edge as invisible.
			return new Rectangle[] {};
			// TODO: We could come up with a more decent strategy here,
			// which also computes clipping fragments in those cases
			// where source/target are not visible but the edge
			// intersects with the enclosing source/target viewport's
			// parents bounds.

		}
		// both ends are visible, so just return what we have
		// computed before
		// (clipping at nearest enclosing viewport)
		return new Rectangle[] { clipRect };
	}

	/**
	 * Computes clipping rectangle for a given (node) figure. Will consider all
	 * enclosing viewports, excluding the root viewport.
	 */
	protected Rectangle getNodeClippingRectangle(IFigure figure) {
		// start with the bounds of the edit part's figure
		Rectangle clipRect = getAbsoluteBoundsAsCopy(figure);

		// now traverse the viewport path of the figure (and reduce clipRect
		// to what is actually visible); process all viewports up to the
		// root viewport
		List<Viewport> enclosingViewportsPath = ViewportUtilities
				.getViewportsPath(ViewportUtilities.getNearestEnclosingViewport(figure), getRootViewport(), false);
		clipAtViewports(clipRect, enclosingViewportsPath);
		return clipRect;
	}

	/**
	 * Clips the given clipRect at all given viewports.
	 */
	protected void clipAtViewports(Rectangle clipRect, List<Viewport> enclosingViewportsPath) {
		enclosingViewportsPath.forEach(vp -> clipRect.intersect(getAbsoluteViewportAreaAsCopy(vp)));
	}

	/**
	 * Returns the root viewport, i.e. the nearest enclosing viewport of the
	 * connection layer, which corresponds to the nearest enclosing common viewport
	 * of primary and connection layer.
	 */
	protected Viewport getRootViewport() {
		return ViewportUtilities.getNearestEnclosingViewport(connectionLayer);
	}

	/**
	 * Returns the connection's points in absolute coordinates.
	 */
	@SuppressWarnings("static-method")
	protected PointList getAbsolutePointsAsCopy(Connection connection) {
		PointList points = connection.getPoints().getCopy();
		connection.translateToAbsolute(points);
		return points;
	}

	/**
	 * Returns the area covered by the viewport in absolute coordinates.
	 */
	protected Rectangle getAbsoluteViewportAreaAsCopy(Viewport viewport) {
		return getAbsoluteClientAreaAsCopy(viewport);
	}

	/**
	 * Returns the viewport's client area in absolute coordinates.
	 */
	@SuppressWarnings("static-method")
	protected Rectangle getAbsoluteClientAreaAsCopy(IFigure figure) {
		Rectangle absoluteClientArea = figure.getClientArea();
		figure.translateToParent(absoluteClientArea);
		figure.translateToAbsolute(absoluteClientArea);
		return absoluteClientArea;
	}

	/**
	 * Returns the figure's bounds in absolute coordinates.
	 */
	@SuppressWarnings("static-method")
	protected Rectangle getAbsoluteBoundsAsCopy(IFigure figure) {
		Rectangle absoluteFigureBounds = figure.getBounds().getCopy();
		figure.translateToAbsolute(absoluteFigureBounds);
		return absoluteFigureBounds;
	}
}